package api

import (
	"bytes"
	"errors"
	"fmt"
	"gitee.com/gopher2011/gva/server/dao"
	"gitee.com/gopher2011/gva/server/util"
	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"github.com/unrolled/secure"
	"go.uber.org/zap"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"
)

func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		// jwt鉴权取头部 x-token 这个键的值,登录时返回token信息,这里前端需要把token存储到cookie或者本地localStorage中,不过需要跟后端协商过期时间,可以约定刷新token,或者重新登陆!
		token := c.Request.Header.Get("x-token")
		if token == "" {
			FailWithDetailed(c,"未登陆或者非法登陆",gin.H{"reload": true}) // 服务端返回给前端的 reload == true,说明前端的请求中没有携带token,因此,客户端需要重新登陆(即:重新获取token)
			c.Abort()
			return
		}
		// token是否在黑名单内
		exist := dao.JwtBlackListDao.Exist(token)
		if exist.Id >0 {
			FailWithDetailed(c,"您的帐户异地登陆或令牌失效",gin.H{"reload": true})
			c.Abort()
			return
		}
		j := NewJwt()
		// parseToken 解析token包含的信息
		claims, err := j.ParseToken(token)
		if err != nil {
			if err == TokenExpired {
				FailWithDetailed(c,"授权已过期",gin.H{"reload": true})
				c.Abort()
				return
			}
			FailWithMessage(c,"服务器内部错误")
			c.Abort()
			return
		}

		// 如果从当前token中解析得到的User信息,不是数据库中保存的User信息,则将当前token加入黑名单!
		if user := dao.SysUserDao.FindUserByUuid(claims.UUID); user.Id <=0 {
			dao.JwtBlackListDao.Insert(dao.JwtBlackList{Token: token})
			FailWithDetailed(c,err.Error(),gin.H{"reload": true})//整因为当前token认证无效,所以才将 reload 设置为 true
			c.Abort()
		}

		// 刷新token
		if claims.ExpiresAt-time.Now().Unix() < claims.BufferTime {
			claims.ExpiresAt = time.Now().Unix() + dao.Cfg.JWT.ExpiresTime
			newToken, _ := j.CreateToken(*claims)
			newClaims, _ := j.ParseToken(newToken)
			c.Header("new-token", newToken)
			c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt, 10))
			if dao.Cfg.System.UseMultipoint {
				redisJwtToken, err := dao.JwtBlackListDao.GetRedisJwt(newClaims.Username) //从redis中取jwt
				if err != nil {
					dao.Log.Error("get redis jwt failed", zap.Any("err", err))
				} else { // 当之前的取成功时才进行拉黑操作
					dao.JwtBlackListDao.Insert(dao.JwtBlackList{Token: redisJwtToken})
				}
				// 无论如何都要记录当前的活跃状态
				dao.JwtBlackListDao.SetRedisJwt(newToken, newClaims.Username)
			}
		}
		c.Set("claims", claims)
		c.Next()
	}
}

var (
	TokenExpired     = errors.New(" Token过期了!")
	TokenNotValidYet = errors.New(" Token未激活")
	TokenMalformed   = errors.New("这不是Token")
	TokenInvalid     = errors.New("无法处理此Token")
)

type JWT struct {
	SigningKey []byte //token 的签名
}

func NewJwt() *JWT {
	return &JWT{
		[]byte(dao.Cfg.JWT.SigningKey), //从配置文件中读取SigningKey
	}
}

// 创建一个 token 参数<dao.JwtClaims>是需要被token保护的信息
func (j *JWT) CreateToken(claims dao.JwtClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(j.SigningKey)
}

// 解析token
func (j *JWT) ParseToken(tokenStr string) (*dao.JwtClaims, error) {
	token, err := jwt.ParseWithClaims(tokenStr, &dao.JwtClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return j.SigningKey, nil
	})
	if err != nil {
		if v, ok := err.(*jwt.ValidationError); ok {
			if v.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, TokenMalformed
			} else if v.Errors&jwt.ValidationErrorExpired != 0 {
				// token过期了
				return nil, TokenExpired
			} else if v.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, TokenNotValidYet
			} else {
				return nil, TokenInvalid
			}
		}
	}
	if token != nil {
		if claims, ok := token.Claims.(*dao.JwtClaims); ok && token.Valid {
			return claims, nil
		}
		return nil, TokenInvalid
	} else {
		return nil, TokenInvalid
	}
}

// 拦截器(拦截没有权限的请求)
func Casbin() gin.HandlerFunc {
	return func(c *gin.Context) {
		claims, _ := c.Get("claims")
		waitUse := claims.(*dao.JwtClaims)
		// 获取请求的主体(用户角色)
		sub := waitUse.AuthorityId
		// 获取被请求的资源(请求的URI)
		obj := c.Request.URL.RequestURI()
		// 获取具体的操作类别(请求的方法)
		act := c.Request.Method

		e := dao.CasbinRuleDao.Casbin()

		// 判断 casbin_rule表中是否存在当前策略
		ok, _ := e.Enforce(sub, obj, act)

		// 如果是开发者模式,则不拦截当前请求!
		if dao.Cfg.System.Env == "develop" || ok {
			c.Next()
		} else {
			FailWithDetailed(c,"权限不足",gin.H{})
			c.Abort()
			return
		}
	}
}

// 详细记录当前用户的请求,并将错误信息发送到指定邮箱!
func Error2Email() gin.HandlerFunc {
	return func(c *gin.Context) {
		var username string
		// 从请求中的 claims中取用户名
		if claims, ok := c.Get("claims"); ok {
			use := claims.(*dao.JwtClaims)
			username = use.Username
		} else {
			user := dao.SysUserDao.Select(c.Request.Header.Get("x-user-id"))
			if user.Id <=0  {
				username = "Unknown"
			}
			username = user.Username
		}
		body, _ := ioutil.ReadAll(c.Request.Body) //读取请求体中的数据
		record := dao.SysOperationRecord{
			Ip:     c.ClientIP(),
			Method: c.Request.Method,
			Path:   c.Request.URL.Path,
			Agent:  c.Request.UserAgent(),
			Body:   string(body),
		}
		now := time.Now()
		c.Next()
		latency := time.Now().Sub(now)
		status := c.Writer.Status()
		record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
		str := "接收到的请求为" + record.Body + "\n" + "请求方式为" + record.Method + "\n" + "报错信息如下" + record.ErrorMessage + "\n" + "耗时" + latency.String() + "\n"
		if status != 200 {
			sub := username + "" + record.Ip + "调用了" + record.Path + "报错了"
			// 将错误以邮件的形式发送到指定主机
			if err := util.Error2Email(sub, str); err != nil {
				dao.Log.Error("ErrorToEmail Failed, err:", zap.Any("err", err))
			}
		}
	}
}

// recover项目可能出现的panic，并使用zap记录相关日志
func Recovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			// recover()仅在延迟函数 defer 中有效，在正常的执行过程中，调用 recover 会返回 nil 并且没有其他任何效果，如果当前的 goroutine 陷入恐慌，调用 recover 可以捕获到 panic 的输入值，并且恢复正常的执行。
			if err := recover(); err != nil {
				//检查连接是否断开，因为这并不是保证紧急堆栈跟踪的真正条件。
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok { //如果错误是:网络链接错误
					if se, ok := ne.Err.(*os.SyscallError); ok { //如果网络链接错误,是因为系统调用的错误。
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				httpRequest, _ := httputil.DumpRequest(c.Request, false) //服务器端调试客户端的请求,如果第二个参数为false,说明没有请求体
				if brokenPipe {
					dao.Log.Error(c.Request.URL.Path, zap.Any("error", err), zap.String("request", string(httpRequest)))
					_ = c.Error(err.(error))
					c.Abort()
					return
				}

				if stack {
					dao.Log.Error("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)), zap.String("stack", string(debug.Stack())))
				} else {
					dao.Log.Error("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)))
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

// 通过这种中间件的形式,将端口转换为https协议!
//	博客链接: https://blog.csdn.net/duqiming/article/details/89176903
func LoadTls() gin.HandlerFunc {
	return func(c *gin.Context) {
		middle := secure.New(secure.Options{
			SSLRedirect: true,
			SSLHost:     "localhost:443",
		})
		err := middle.Process(c.Writer, c.Request)
		if err != nil {
			// 如果出现错误，请不要继续
			fmt.Println(err)
			return
		}
		// 继续往下处理
		c.Next()
	}
}


// 处理跨域请求,支持options访问
func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		origin := c.Request.Header.Get("Origin")
		c.Header("Access-Control-Allow-Origin", origin)
		c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id")
		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
		c.Header("Access-Control-Allow-Credentials", "true")

		// 放行所有OPTIONS方法
		if method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
		}
		// 处理请求
		c.Next()
	}
}

func OperationRecord() gin.HandlerFunc {
	return func(c *gin.Context) {
		var body []byte
		var userId int
		if c.Request.Method != http.MethodGet {
			var err error
			body, err = ioutil.ReadAll(c.Request.Body)
			if err != nil {
				dao.Log.Error("read body from request error:", zap.Any("err", err))
			} else {
				c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
			}
		}
		if claims, ok := c.Get("claims"); ok { //获取保存在请求中的 claims信息,claims这个键保存的值是将要被jwt加密的用户信息
			user := claims.(*dao.JwtClaims)
			userId = int(user.ID)
		} else {
			id, err := strconv.Atoi(c.Request.Header.Get("x-user-id"))
			if err != nil {
				userId = 0
			}
			userId = id
		}

		record := dao.SysOperationRecord{
			Ip:     c.ClientIP(),
			Method: c.Request.Method,
			Path:   c.Request.URL.Path,
			Agent:  c.Request.UserAgent(),
			Body:   string(body),
			UserId: userId,
		}

		// 这个结构体是 c.Writer的父类
		writer := responseBodyWriter{
			ResponseWriter: c.Writer,
			body:           &bytes.Buffer{},
		}
		c.Writer = writer
		now := time.Now()
		c.Next()
		latency := time.Now().Sub(now) //计算当前时间与 now的差值,即:请求延迟!
		record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
		record.Status = c.Writer.Status()
		record.Latency = latency.Milliseconds()
		record.Resp = writer.body.String()
		res := dao.SysOperationRecordDao.Insert(record)
		if res <=0 {
			dao.Log.Error("create operation record failed:")
		}
	}
}

// 向客户端响应数据的回写结构体
type responseBodyWriter struct {
	gin.ResponseWriter
	body *bytes.Buffer
}

func (r responseBodyWriter) Write(b []byte) (int, error) {
	r.body.Write(b)
	return r.ResponseWriter.Write(b)
}
